/*********************************************************************
 * Copyright (C) 2002 Andrew Khan
 * <p>
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * <p>
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * <p>
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ***************************************************************************/

package jxl.read.biff;

import jxl.WorkbookSettings;
import jxl.biff.BaseCompoundFile;
import jxl.biff.IntegerHelper;
import jxl.common.Logger;

import java.util.ArrayList;
import java.util.Iterator;

/**
 * Reads in and defrags an OLE compound compound file
 * (Made public only for the PropertySets demo)
 */
public final class CompoundFile extends BaseCompoundFile {
    /**
     * The logger
     */
    private static Logger logger = Logger.getLogger(CompoundFile.class);

    /**
     * The original OLE stream, organized into blocks, which can
     * appear at any physical location in the file
     */
    private byte[] data;
    /**
     * The number of blocks it takes to store the big block depot
     */
    private int numBigBlockDepotBlocks;
    /**
     * The start block of the small block depot
     */
    private int sbdStartBlock;
    /**
     * The start block of the root entry
     */
    private int rootStartBlock;
    /**
     * The header extension block
     */
    private int extensionBlock;
    /**
     * The number of header extension blocks
     */
    private int numExtensionBlocks;
    /**
     * The root entry
     */
    private byte[] rootEntry;
    /**
     * The sequence of blocks which comprise the big block chain
     */
    private int[] bigBlockChain;
    /**
     * The sequence of blocks which comprise the small block chain
     */
    private int[] smallBlockChain;
    /**
     * The chain of blocks which comprise the big block depot
     */
    private int[] bigBlockDepotBlocks;
    /**
     * The list of property sets
     */
    private ArrayList propertySets;

    /**
     * The workbook settings
     */
    private WorkbookSettings settings;

    /**
     * The property storage root entry
     */
    private PropertyStorage rootEntryPropertyStorage;

    /**
     * Initializes the compound file
     *
     * @param d the raw data of the ole stream
     * @param ws the workbook settings
     * @exception BiffException
     */
    public CompoundFile(byte[] d, WorkbookSettings ws) throws BiffException {
        super();
        data = d;
        settings = ws;

        // First verify the OLE identifier
        for (int i = 0; i < IDENTIFIER.length; i++) {
            if (data[i] != IDENTIFIER[i]) {
                throw new BiffException(BiffException.unrecognizedOLEFile);
            }
        }

        propertySets = new ArrayList();
        numBigBlockDepotBlocks = IntegerHelper.getInt
                (data[NUM_BIG_BLOCK_DEPOT_BLOCKS_POS],
                        data[NUM_BIG_BLOCK_DEPOT_BLOCKS_POS + 1],
                        data[NUM_BIG_BLOCK_DEPOT_BLOCKS_POS + 2],
                        data[NUM_BIG_BLOCK_DEPOT_BLOCKS_POS + 3]);
        sbdStartBlock = IntegerHelper.getInt
                (data[SMALL_BLOCK_DEPOT_BLOCK_POS],
                        data[SMALL_BLOCK_DEPOT_BLOCK_POS + 1],
                        data[SMALL_BLOCK_DEPOT_BLOCK_POS + 2],
                        data[SMALL_BLOCK_DEPOT_BLOCK_POS + 3]);
        rootStartBlock = IntegerHelper.getInt
                (data[ROOT_START_BLOCK_POS],
                        data[ROOT_START_BLOCK_POS + 1],
                        data[ROOT_START_BLOCK_POS + 2],
                        data[ROOT_START_BLOCK_POS + 3]);
        extensionBlock = IntegerHelper.getInt
                (data[EXTENSION_BLOCK_POS],
                        data[EXTENSION_BLOCK_POS + 1],
                        data[EXTENSION_BLOCK_POS + 2],
                        data[EXTENSION_BLOCK_POS + 3]);
        numExtensionBlocks = IntegerHelper.getInt
                (data[NUM_EXTENSION_BLOCK_POS],
                        data[NUM_EXTENSION_BLOCK_POS + 1],
                        data[NUM_EXTENSION_BLOCK_POS + 2],
                        data[NUM_EXTENSION_BLOCK_POS + 3]);

        bigBlockDepotBlocks = new int[numBigBlockDepotBlocks];

        int pos = BIG_BLOCK_DEPOT_BLOCKS_POS;

        int bbdBlocks = numBigBlockDepotBlocks;

        if (numExtensionBlocks != 0) {
            bbdBlocks = (BIG_BLOCK_SIZE - BIG_BLOCK_DEPOT_BLOCKS_POS) / 4;
        }

        for (int i = 0; i < bbdBlocks; i++) {
            bigBlockDepotBlocks[i] = IntegerHelper.getInt
                    (d[pos], d[pos + 1], d[pos + 2], d[pos + 3]);
            pos += 4;
        }

        for (int j = 0; j < numExtensionBlocks; j++) {
            pos = (extensionBlock + 1) * BIG_BLOCK_SIZE;
            int blocksToRead = Math.min(numBigBlockDepotBlocks - bbdBlocks,
                    BIG_BLOCK_SIZE / 4 - 1);

            for (int i = bbdBlocks; i < bbdBlocks + blocksToRead; i++) {
                bigBlockDepotBlocks[i] = IntegerHelper.getInt
                        (d[pos], d[pos + 1], d[pos + 2], d[pos + 3]);
                pos += 4;
            }

            bbdBlocks += blocksToRead;
            if (bbdBlocks < numBigBlockDepotBlocks) {
                extensionBlock = IntegerHelper.getInt
                        (d[pos], d[pos + 1], d[pos + 2], d[pos + 3]);
            }
        }

        readBigBlockDepot();
        readSmallBlockDepot();

        rootEntry = readData(rootStartBlock);
        readPropertySets();
    }

    /**
     * Reads the big block depot entries
     */
    private void readBigBlockDepot() {
        int pos = 0;
        int index = 0;
        bigBlockChain = new int[numBigBlockDepotBlocks * BIG_BLOCK_SIZE / 4];

        for (int i = 0; i < numBigBlockDepotBlocks; i++) {
            pos = (bigBlockDepotBlocks[i] + 1) * BIG_BLOCK_SIZE;

            for (int j = 0; j < BIG_BLOCK_SIZE / 4; j++) {
                bigBlockChain[index] = IntegerHelper.getInt
                        (data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
                pos += 4;
                index++;
            }
        }
    }

    /**
     * Reads the small block chain's depot entries
     */
    private void readSmallBlockDepot() throws BiffException {
        int pos = 0;
        int index = 0;
        int sbdBlock = sbdStartBlock;
        smallBlockChain = new int[0];

        // Some non-excel generators specify -1 for an empty small block depot
        // simply warn and return
        if (sbdBlock == -1) {
            logger.warn("invalid small block depot number");
            return;
        }

        int blockCount = 0;
        for (; blockCount <= bigBlockChain.length && sbdBlock != -2; blockCount++) {
            // Allocate some more space to the small block chain
            int[] oldChain = smallBlockChain;
            smallBlockChain = new int[smallBlockChain.length + BIG_BLOCK_SIZE / 4];
            System.arraycopy(oldChain, 0, smallBlockChain, 0, oldChain.length);

            pos = (sbdBlock + 1) * BIG_BLOCK_SIZE;

            for (int j = 0; j < BIG_BLOCK_SIZE / 4; j++) {
                smallBlockChain[index] = IntegerHelper.getInt
                        (data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
                pos += 4;
                index++;
            }

            sbdBlock = bigBlockChain[sbdBlock];
        }

        if (blockCount > bigBlockChain.length) {
            // Attempted to read more blocks than the block chain contains entries
            // for.  This indicates a loop in the chain
            throw new BiffException(BiffException.corruptFileFormat);
        }
    }

    /**
     * Reads all the property sets
     */
    private void readPropertySets() {
        int offset = 0;
        byte[] d = null;

        while (offset < rootEntry.length) {
            d = new byte[PROPERTY_STORAGE_BLOCK_SIZE];
            System.arraycopy(rootEntry, offset, d, 0, d.length);
            PropertyStorage ps = new PropertyStorage(d);

            // sometimes the MAC Operating system leaves some property storage
            // names blank.  Contributed by Jacky
            if (ps.name == null || ps.name.length() == 0) {
                if (ps.type == ROOT_ENTRY_PS_TYPE) {
                    ps.name = ROOT_ENTRY_NAME;
                    logger.warn("Property storage name for " + ps.type +
                            " is empty - setting to " + ROOT_ENTRY_NAME);
                } else {
                    if (ps.size != 0) {
                        logger.warn("Property storage type " + ps.type +
                                " is non-empty and has no associated name");
                    }
                }
            }
            propertySets.add(ps);
            if (ps.name.equalsIgnoreCase(ROOT_ENTRY_NAME)) {
                rootEntryPropertyStorage = ps;
            }
            offset += PROPERTY_STORAGE_BLOCK_SIZE;
        }

        if (rootEntryPropertyStorage == null) {
            rootEntryPropertyStorage = (PropertyStorage) propertySets.get(0);
        }
    }

    /**
     * Gets the defragmented stream from this ole compound file
     *
     * @param streamName the stream name to get
     * @return the defragmented ole stream
     * @exception BiffException
     */
    public byte[] getStream(String streamName) throws BiffException {
        PropertyStorage ps = findPropertyStorage(streamName,
                rootEntryPropertyStorage);

        // Property set can't be found from the direct hierarchy, so just
        // search on the name
        if (ps == null) {
            ps = getPropertyStorage(streamName);
        }

        if (ps.size >= SMALL_BLOCK_THRESHOLD ||
                streamName.equalsIgnoreCase(ROOT_ENTRY_NAME)) {
            return getBigBlockStream(ps);
        } else {
            return getSmallBlockStream(ps);
        }
    }

    /**
     * Gets the defragmented stream from this ole compound file.  Used when
     * copying workbooks with macros
     *
     * @param psIndex the property storage index
     * @return the defragmented ole stream
     * @exception BiffException
     */
    public byte[] getStream(int psIndex) throws BiffException {
        PropertyStorage ps = getPropertyStorage(psIndex);

        if (ps.size >= SMALL_BLOCK_THRESHOLD ||
                ps.name.equalsIgnoreCase(ROOT_ENTRY_NAME)) {
            return getBigBlockStream(ps);
        } else {
            return getSmallBlockStream(ps);
        }
    }

    /**
     * Recursively searches the property storages in hierarchy order
     * for the appropriate name.  This is the public version which is
     * invoked from the writable version
     * when copying a sheet with addition property sets.
     */
    public PropertyStorage findPropertyStorage(String name) {
        return findPropertyStorage(name, rootEntryPropertyStorage);
    }

    /**
     * Recursively searches the property storages in hierarchy order
     * for the appropriate name.
     */
    private PropertyStorage findPropertyStorage(String name,
                                                PropertyStorage base) {
        if (base.child == -1) {
            return null;
        }

        // Get the child
        PropertyStorage child = getPropertyStorage(base.child);
        if (child.name.equalsIgnoreCase(name)) {
            return child;
        }

        // Find the previous property storages on the same level
        PropertyStorage prev = child;
        while (prev.previous != -1) {
            prev = getPropertyStorage(prev.previous);
            if (prev.name.equalsIgnoreCase(name)) {
                return prev;
            }
        }

        // Find the next property storages on the same level
        PropertyStorage next = child;
        while (next.next != -1) {
            next = getPropertyStorage(next.next);
            if (next.name.equalsIgnoreCase(name)) {
                return next;
            }
        }

        return findPropertyStorage(name, child);
    }

    /**
     * Gets the property set with the specified name
     * @param name the property storage name
     * @return the property storage record
     * @exception BiffException
     * @deprecated remove me
     */
    private PropertyStorage getPropertyStorage(String name)
            throws BiffException {
        // Find the workbook property
        Iterator i = propertySets.iterator();
        boolean found = false;
        boolean multiple = false;
        PropertyStorage ps = null;
        while (i.hasNext()) {
            PropertyStorage ps2 = (PropertyStorage) i.next();
            if (ps2.name.equalsIgnoreCase(name)) {
                multiple = found == true ? true : false;
                found = true;
                ps = ps2;
            }
        }

        if (multiple) {
            logger.warn("found multiple copies of property set " + name);
        }

        if (!found) {
            throw new BiffException(BiffException.streamNotFound);
        }

        return ps;
    }

    /**
     * Gets the property set with the specified name
     * @param index the index of the property storage
     * @return the property storage record
     */
    private PropertyStorage getPropertyStorage(int index) {
        return (PropertyStorage) propertySets.get(index);
    }

    /**
     * Build up the resultant stream using the big blocks
     *
     * @param ps the property storage
     * @return the big block stream
     */
    private byte[] getBigBlockStream(PropertyStorage ps) {
        int numBlocks = ps.size / BIG_BLOCK_SIZE;
        if (ps.size % BIG_BLOCK_SIZE != 0) {
            numBlocks++;
        }

        byte[] streamData = new byte[numBlocks * BIG_BLOCK_SIZE];

        int block = ps.startBlock;

        int count = 0;
        int pos = 0;
        while (block != -2 && count < numBlocks) {
            pos = (block + 1) * BIG_BLOCK_SIZE;
            System.arraycopy(data, pos, streamData,
                    count * BIG_BLOCK_SIZE, BIG_BLOCK_SIZE);
            count++;
            block = bigBlockChain[block];
        }

        if (block != -2 && count == numBlocks) {
            logger.warn("Property storage size inconsistent with block chain.");
        }

        return streamData;
    }

    /**
     * Build up the resultant stream using the small blocks
     * @param ps the property storage
     * @return the data
     * @exception BiffException
     */
    private byte[] getSmallBlockStream(PropertyStorage ps)
            throws BiffException {
        byte[] rootdata = readData(rootEntryPropertyStorage.startBlock);
        byte[] sbdata = new byte[0];

        int block = ps.startBlock;
        int pos = 0;

        int blockCount = 0;
        for (; blockCount <= smallBlockChain.length && block != -2; blockCount++) {
            // grow the array
            byte[] olddata = sbdata;
            sbdata = new byte[olddata.length + SMALL_BLOCK_SIZE];
            System.arraycopy(olddata, 0, sbdata, 0, olddata.length);

            // Copy in the new data
            pos = block * SMALL_BLOCK_SIZE;
            System.arraycopy(rootdata, pos, sbdata,
                    olddata.length, SMALL_BLOCK_SIZE);
            block = smallBlockChain[block];

            if (block == -1) {
                logger.warn("Incorrect terminator for small block stream " + ps.name);
                block = -2; // kludge to force the loop termination
            }
        }

        if (blockCount > smallBlockChain.length) {
            // Attempted to read more blocks than the block chain contains entries
            // for. This indicates a loop in the chain
            throw new BiffException(BiffException.corruptFileFormat);
        }

        return sbdata;
    }

    /**
     * Reads the block chain from the specified block and returns the
     * data as a continuous stream of bytes
     *
     * @param bl the block number
     * @return the data
     */
    private byte[] readData(int bl) throws BiffException {
        int block = bl;
        int pos = 0;
        byte[] entry = new byte[0];

        int blockCount = 0;
        for (; blockCount <= bigBlockChain.length && block != -2; blockCount++) {
            // Grow the array
            byte[] oldEntry = entry;
            entry = new byte[oldEntry.length + BIG_BLOCK_SIZE];
            System.arraycopy(oldEntry, 0, entry, 0, oldEntry.length);
            pos = (block + 1) * BIG_BLOCK_SIZE;
            System.arraycopy(data, pos, entry,
                    oldEntry.length, BIG_BLOCK_SIZE);
            if (bigBlockChain[block] == block) {
                throw new BiffException(BiffException.corruptFileFormat);
            }
            block = bigBlockChain[block];
        }

        if (blockCount > bigBlockChain.length) {
            // Attempted to read more blocks than the block chain contains entries
            // for.  This indicates a loop in the chain
            throw new BiffException(BiffException.corruptFileFormat);
        }

        return entry;
    }

    /**
     * Gets the number of property sets
     * @return the number of property sets
     */
    public int getNumberOfPropertySets() {
        return propertySets.size();
    }

    /**
     * Gets the property set.  Invoked when copying worksheets with macros.
     * Simply calls the private counterpart
     *
     * @param ps the property set name
     * @return the property set with the given name
     */
    public PropertyStorage getPropertySet(int index) {
        return getPropertyStorage(index);
    }
}












